package com.gitee.qdbp.tools.utils;

/**
 * 缩进工具类<br>
 * \r=CARRIAGE RETURN,回到行首; \n=LINE FEED,换行; \t=HORIZONTAL TABULATION,水平制表位<br>
 * \f=FORM FEED,换页, 这个不作处理, 相当于一个不可见字符
 *
 * @author zhaohuihua
 * @version 20200716
 * @since 5.0.0
 */
public class IndentTools {

    public static final Operator tabs = new Operator(true);
    public static final Operator space = new Operator(false);

    /** 获取缩进字符 **/
    public static char[] getIndenTabs(int size) {
        return getIndenTabs(size, false);
    }

    /** 获取缩进字符 **/
    public static char[] getIndenTabs(int size, boolean useTab) {
        if (size <= 0) {
            return new char[0];
        }
        if (useTab) {
            if (size == 1) {
                return new char[] { '\t' };
            } else {
                char[] tabs = new char[size];
                for (int i = 0; i < size; i++) {
                    tabs[i] = '\t';
                }
                return tabs;
            }
        } else {
            char[] spaces = new char[] { ' ', ' ', ' ', ' ' };
            if (size == 1) {
                return spaces;
            } else {
                int length = spaces.length;
                char[] tabs = new char[size * length];
                for (int i = 0, z = tabs.length; i < z; i += length) {
                    System.arraycopy(spaces, 0, tabs, i, length);
                }
                return tabs;
            }
        }
    }

    /**
     * 所有缩进设置为n个TAB, 以最小的缩进为基准
     * 
     * @param string 字符串
     * @param size 缩进量: 必须为正数或0, 负数视为0
     * @deprecated 改为{@link Operator#indentAll(String, int, boolean) IndentTools.space.indentAll(String, int)}
     */
    @Deprecated
    public static String indentAll(String string, int size) {
        return space.indentAll(string, size, true);
    }

    /**
     * 所有缩进设置为n个TAB, 以最小的缩进为基准
     * 
     * @param string 字符串
     * @param size 缩进量: 必须为正数或0, 负数视为0
     * @param leading 开头要不要缩进
     * @return 处理后的字符串
     * @deprecated 改为{@link Operator#indentAll(String, int, boolean) IndentTools.space.indentAll(String, int, boolean)}
     */
    @Deprecated
    public static String indentAll(String string, int size, boolean leading) {
        return space.indentAll(string, size, leading);
    }

    /**
     * 在最后一个换行符之后插入信息<pre>
     * 例如: \n=换行符, \t=TAB符, \s=空格
     * \n\tABC\n\t\t --> \n\tABC\n[\tMESSAGE\n]\t\tDEF // 换行模式(插入的信息与上一行缩进对齐)
     * \n\tABC\s     --> \n\tABC\s[\sMESSAGE\s]DEF      // 非换行模式
     * \n\tABC       --> \n\tABC[\sMESSAGE\s]DEF       // 非换行模式</pre>
     * 
     * @param string 原字符串
     * @param message 待插入的信息
     * @deprecated 改为{@link Operator#insertMessageAfterLastNewline(StringBuilder, CharSequence)
     *             IndentTools.space.insertMessageAfterLastNewline(StringBuilder, CharSequence)}
     */
    @Deprecated
    public static void insertMessageAfterLastNewline(StringBuilder string, CharSequence message) {
        space.insertMessageAfterLastNewline(string, message);
    }

    /**
     * 查找缩进量(从字符串最后向前查找最近一行的缩进量)<pre>
     * 例如: \r\n=换行符, \t=TAB符, \s=空格
     * \n\tABC\n\t\tDEF\t\t\t --> 这里要找的是DEF之前的那个换行符之后的空白字符, 即缩进量为2
     * \n\tABC\n\t\tDEF\n     --> 最后一个字符就是换行符, 即刚刚换行完, 要找的仍然是DEF之前的那个换行符
     * \n\tABC\n\t\tDEF\n\n   --> 最后连续多个换行符, 要找的仍然是DEF之前的那个换行符
     * \n\tABC\n\t\t          --> 这里应返回ABC后面的换行符之后的缩进量2
     * \tABC --> 这里应返回首行的缩进量1</pre>
     * 
     * @param string 字符串
     * @return 缩进量
     */
    public static int findLastIndentSize(CharSequence string) {
        int lastIndex = string.length() - 1;
        return findLastIndentSize(string, lastIndex);
    }

    /**
     * 查找缩进量(从指定位置向前查找最近一行的缩进量)<pre>
     * 例如: \r\n=换行符, \t=TAB符, \s=空格
     * \n\tABC\n\t\tDEF\t\t\t --> 这里要找的是DEF之前的那个换行符之后的空白字符, 即缩进量为2
     * \n\tABC\n\t\tDEF\n     --> 最后一个字符就是换行符, 即刚刚换行完, 要找的仍然是DEF之前的那个换行符
     * \n\tABC\n\t\tDEF\n\n   --> 最后连续多个换行符, 要找的仍然是DEF之前的那个换行符
     * \n\tABC\n\t\t          --> 这里应返回ABC后面的换行符之后的缩进量2
     * \tABC --> 这里应返回首行的缩进量1</pre>
     * 
     * @param string 字符串
     * @param lastIndex 从哪个位置开始向前查找
     * @return 缩进量
     */
    public static int findLastIndentSize(CharSequence string, int lastIndex) {
        for (int i = lastIndex; i >= 0; i--) {
            char c = string.charAt(i);
            if (c == '\r' || c == '\n') {
                // 移除最后连续的换行符
                lastIndex--;
            } else {
                break;
            }
        }
        // 查找最后一个换行符
        int position = -1;
        for (int i = lastIndex; i >= 0; i--) {
            char c = string.charAt(i);
            if (c == '\r' || c == '\n') {
                position = i;
                break;
            }
        }
        // 从换行符位置的下一个字符开始, 查找前置空白字符
        // 计算空白字符等于多少缩进量
        return calcSpacesToTabSize(string, position + 1, lastIndex + 1);
    }

    /** 计算空白字符等于多少缩进量(余1个空格不算缩进,余2个空格以上算1个缩进) **/
    public static int calcSpacesToTabSize(CharSequence string) {
        return calcSpacesToTabSize(string, 0, string.length());
    }

    /** 计算空白字符等于多少缩进量(余1个空格不算缩进,余2个空格以上算1个缩进) **/
    public static int calcSpacesToTabSize(CharSequence string, int start) {
        return calcSpacesToTabSize(string, start, string.length());
    }

    /** 计算空白字符等于多少缩进量(余1个空格不算缩进,余2个空格以上算1个缩进) **/
    public static int calcSpacesToTabSize(CharSequence string, int start, int end) {
        int size = 0;
        int pending = 0;
        for (int i = start; i < end; i++) {
            char c = string.charAt(i);
            if (c == ' ') {
                pending++;
            } else if (c == '\t') {
                if (pending > 0) {
                    // TAB之前的空格, 不够4个的不计缩进
                    size += pending / 4;
                    pending = 0;
                }
                size++;
            } else {
                break;
            }
        }
        if (pending > 0) {
            // 余1个空格不算缩进,余2个空格算1个缩进
            size += (pending + 2) / 4;
        }
        return size;
    }

    /**
     * 统计最小的缩进数
     * 
     * @param string 字符串
     * @return 缩进数量
     */
    public static int countMinIndentSize(CharSequence string) {
        return countMinIndentSize(string, true);
    }

    /**
     * 统计最小的缩进数
     * 
     * @param string 字符串
     * @param leading 要不要统计开头的缩进
     * @return 缩进数量, -1表示未找到缩进
     */
    public static int countMinIndentSize(CharSequence string, boolean leading) {
        int minIndent = -1;
        for (int i = 0, z = string.length(); i < z; i++) {
            char c = string.charAt(i);
            if (i == 0 && leading || c == '\r' || c == '\n') {
                int index = skipBlankLines(string, i);
                int indent = calcSpacesToTabSize(string, index);
                if (indent == 0) {
                    return 0; // 已找到最小缩进数量, 可以提前结束
                } else {
                    if (minIndent < 0 || minIndent > indent) {
                        minIndent = indent;
                    }
                }
            }
        }
        return minIndent;
    }

    /** 跳过空白行 **/
    private static int skipBlankLines(CharSequence string, int index) {
        int stopIndex = index;
        char lastChar = 0;
        for (int i = index, z = string.length(); i < z; lastChar = string.charAt(i), i++) {
            char c = string.charAt(i);
            if (c == '\r' || c == '\n') {
                continue;
            } else if (c == ' ' || c == '\t') {
                if (lastChar == '\r' || lastChar == '\n') {
                    stopIndex = i;
                }
                continue;
            } else {
                if (lastChar == '\r' || lastChar == '\n') {
                    stopIndex = i;
                }
                break;
            }
        }
        return stopIndex;
    }

    /** 统计文本中有多少个换行符 **/
    public static int countNewlineChars(String string) {
        // 单独的\r或\n算一行, 连续的\r\n算一行, \n\r算两行
        int count = 0;
        int size = string == null ? 0 : string.length();
        for (int i = 0; i < size; i++) {
            char c = string.charAt(i);
            if (c == '\n') {
                count++;
            } else if (c == '\r') {
                if (i == size - 1 || string.charAt(i + 1) != '\n') {
                    count++;
                }
            }
        }
        return count;
    }

    /** 跳过缩进字符 **/
    private static int skipIndent(CharSequence string, int index, int limit) {
        int count = 0;
        int i = index;
        for (; count < limit && i < string.length(); i++) {
            char c = string.charAt(i);
            if (c == ' ') {
                count++;
            } else if (c == '\t') {
                count = count / 4 * 4 + 4; // 除4再乘4, 清除掉不被4整除的空格
            } else {
                break;
            }
        }
        return i;
    }

    /** 是不是空行, 从指定位置到下一个换行符之间是否全是空白字符 **/
    private static boolean isBlankLine(CharSequence string, int index) {
        if (index >= string.length()) {
            return false; // 指向字符串末尾, 需要添加缩进, 不算空行
        }
        for (int i = index; i < string.length(); i++) {
            char c = string.charAt(i);
            if (c == ' ' || c == '\t') {
                continue;
            } else if (c == '\r' || c == '\n') {
                return true;
            } else {
                return false;
            }
        }
        return true;
    }

    public static class Operator {

        private boolean useTab;

        protected Operator(boolean useTab) {
            this.useTab = useTab;
        }

        public char[] getIndenTabs(int size) {
            return IndentTools.getIndenTabs(size, useTab);
        }

        /**
         * 所有缩进设置为n个TAB, 以最小的缩进为基准
         * 
         * @param string 字符串
         * @param size 缩进量: 必须为正数或0, 负数视为0
         * @return 处理后的字符串
         */
        public String indentAll(String string, int size) {
            return indentAll(string, size, true);
        }

        /**
         * 所有缩进设置为n个TAB, 以最小的缩进为基准
         * 
         * @param string 字符串
         * @param size 缩进量: 必须为正数或0, 负数视为0
         */
        public void indentAll(StringBuilder string, int size) {
            indentAll(string, size, true);
        }

        /**
         * 所有缩进设置为n个TAB, 以最小的缩进为基准
         * 
         * @param string 字符串
         * @param size 缩进量: 必须为正数或0, 负数视为0
         * @param leading 开头要不要缩进
         * @return 处理后的字符串
         */
        public String indentAll(String string, int size, boolean leading) {
            int minIndent = countMinIndentSize(string, leading);
            if (size < 0) {
                size = 0;
            }
            if (minIndent == size) {
                return string;
            } else {
                return tabAll(string, size - minIndent, leading);
            }
        }

        /**
         * 所有缩进设置为n个TAB, 以最小的缩进为基准
         * 
         * @param string 字符串
         * @param size 缩进量: 必须为正数或0, 负数视为0
         * @param leading 开头要不要缩进
         */
        public void indentAll(StringBuilder string, int size, boolean leading) {
            int minIndent = countMinIndentSize(string, leading);
            if (size < 0) {
                size = 0;
            }
            if (minIndent != size) {
                tabAll(string, size - minIndent, leading);
            }
        }

        /**
         * 所有缩进增加或减少n个TAB
         * 
         * @param string 字符串
         * @param size 缩进量: 正数为增加, 负数为减少
         * @return 处理后的字符串
         */
        public String tabAll(String string, int size) {
            return tabAll(string, size, true);
        }

        /**
         * 所有缩进增加或减少n个TAB
         * 
         * @param string 字符串
         * @param size 缩进量: 正数为增加, 负数为减少
         */
        public void tabAll(StringBuilder string, int size) {
            tabAll(string, size, true);
        }

        /**
         * 所有缩进增加或减少n个TAB
         * 
         * @param string 字符串
         * @param size 缩进量: 正数为增加, 负数为减少
         * @param leading 开头要不要缩进
         * @return 处理后的字符串
         */
        public String tabAll(String string, int size, boolean leading) {
            StringBuilder buffer = new StringBuilder(string);
            tabAll(buffer, size, leading);
            return buffer.toString();
        }

        /**
         * 所有缩进增加或减少n个TAB
         * 
         * @param string 字符串
         * @param size 缩进量: 正数为增加, 负数为减少
         * @param leading 开头要不要缩进
         */
        public void tabAll(StringBuilder string, int size, boolean leading) {
            if (size == 0) {
                return;
            }
            // 如果leading=true, 通过在最前面加一个换行符以便统一处理, 处理完成之后再删除该换行符
            if (leading) {
                string.insert(0, '\n');
            }
            if (size > 0) {
                char[] tabs = getIndenTabs(size);
                for (int i = string.length() - 1; i >= 0; i--) {
                    if (string.charAt(i) == '\n' && !isBlankLine(string, i + 1)) {
                        string.insert(i + 1, tabs);
                    }
                }
            } else {
                int tabs = -size * 4;
                for (int i = string.length() - 1; i >= 0; i--) {
                    if (string.charAt(i) == '\n' && !isBlankLine(string, i + 1)) {
                        // 先查找换行后有多少个空白字符
                        int index = skipIndent(string, i + 1, tabs);
                        // 删除空白字符
                        if (index > i + 1) {
                            string.delete(i + 1, index);
                        }
                    }
                }
            }
            if (leading) {
                string.delete(0, 1);
            }
        }

        /**
         * 在最后一个换行符之后插入信息<pre>
         * 例如: \n=换行符, \t=TAB符, \s=空格
         * \n\tABC\n\t\t --> \n\tABC\n[\tMESSAGE\n]\t\tDEF // 换行模式(插入的信息与上一行缩进对齐)
         * \n\tABC\s     --> \n\tABC\s[\sMESSAGE\s]DEF      // 非换行模式
         * \n\tABC       --> \n\tABC[\sMESSAGE\s]DEF       // 非换行模式</pre>
         * 
         * @param string 原字符串
         * @param message 待插入的信息
         */
        public String insertMessageAfterLastNewline(String string, CharSequence message) {
            StringBuilder buffer = new StringBuilder(string);
            insertMessageAfterLastNewline(buffer, message);
            return buffer.toString();
        }

        /**
         * 在最后一个换行符之后插入信息<pre>
         * 例如: \n=换行符, \t=TAB符, \s=空格
         * \n\tABC\n\t\t --> \n\tABC\n[\tMESSAGE\n]\t\tDEF // 换行模式(插入的信息与上一行缩进对齐)
         * \n\tABC\s     --> \n\tABC\s[\sMESSAGE\s]DEF      // 非换行模式
         * \n\tABC       --> \n\tABC[\sMESSAGE\s]DEF       // 非换行模式</pre>
         * 
         * @param string 原字符串
         * @param message 待插入的信息
         */
        public void insertMessageAfterLastNewline(StringBuilder string, CharSequence message) {
            int last = string.length() - 1;
            int position = -1;
            // 查找插入点: 最后一个换行符之后的那个位置
            // 如果还没找到换行符就遇到了非空字符, 就是非换行模式
            for (int i = last; i >= 0; i--) {
                char c = string.charAt(i);
                if (c == ' ' || c == '\t') {
                    continue;
                } else if (c == '\r' || c == '\n') {
                    position = i;
                    break; // 位置找到了
                } else {
                    break; // 还没找到换行符就遇到了非空字符
                }
            }
            if (position < 0) { // 非换行模式
                string.append(' ').append(message).append(' ');
            } else { // 换行模式
                int indent = findLastIndentSize(string, position);
                StringBuilder temp = new StringBuilder(message);
                if (indent > 0) {
                    temp.insert(0, getIndenTabs(indent));
                }
                temp.append('\n');
                // +1是为了插入到换行符之后
                string.insert(position + 1, temp);
            }
        }
    }
}
